/**
 * 
 */
package com.netflexitysolutions.amazonws.ec2.internal.operations;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;

import javax.xml.bind.JAXBException;
import javax.xml.soap.SOAPFault;
import javax.xml.ws.soap.SOAPFaultException;

import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.ec2.doc._2009_04_04.AmazonEC2PortType;
import com.netflexitysolutions.amazonws.ec2.EC2Exception;
import com.netflexitysolutions.amazonws.ec2.RestEC2ServiceFactory;
import com.netflexitysolutions.amazonws.ec2.SoapEC2ServiceFactory;

/**
 * @author netflexity
 *
 */
public abstract class ExperimantalOperationExecutor<SOAP_REQUEST, EC2_RESPONSE, OPERATION_RESPONSE> {

	public static final String UNAVAILABLE = "Unavailable";

	public static final String INSUFFICIENT_INSTANCE_CAPACITY = "InsufficientInstanceCapacity";

	public static final String INSUFFICIENT_ADDRESS_CAPACITY = "InsufficientAddressCapacity";

	public static final String INSUFFICIENT_RESERVED_INSTANCE_CAPACITY = "InsufficientReservedInstanceCapacity";

	public static final String INTERNAL_ERROR = "InternalError";

	private Logger logger = LoggerFactory.getLogger(ExperimantalOperationExecutor.class);
	
	private Class<SOAP_REQUEST> soapRequestClass;
	
	private Class<EC2_RESPONSE> ec2ResponseClass;
	
	private Class<OPERATION_RESPONSE> operationResponseClass;
	
	private SoapEC2ServiceFactory soapService;
	
	private RestEC2ServiceFactory restService;
	
	protected int maxRetries;
	
    /**
     * @param simpleDB
     */
    @SuppressWarnings("unchecked")
	public ExperimantalOperationExecutor(Object ec2Service) {
    	
    	if (ec2Service instanceof SoapEC2ServiceFactory && ec2Service instanceof RestEC2ServiceFactory) {
    		if (((SoapEC2ServiceFactory)ec2Service).getService() == null) {
    			restService = (RestEC2ServiceFactory)ec2Service;
    		} else {
    			//We prefer soap over http
    			soapService = (SoapEC2ServiceFactory)ec2Service;
    		}
    	} else if(ec2Service instanceof SoapEC2ServiceFactory) {
			soapService = (SoapEC2ServiceFactory)ec2Service;
    	} else if(ec2Service instanceof RestEC2ServiceFactory) {
    		restService = (RestEC2ServiceFactory)ec2Service;
    	} else {
    		throw new IllegalArgumentException("Service should be either SoapEC2Service or RestEC2Service.");
    	}
    	
    	soapRequestClass = (Class<SOAP_REQUEST>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    	ec2ResponseClass = (Class<EC2_RESPONSE>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1];
    	//operationResponseClass = (Class<OPERATION_RESPONSE>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[2];
	}
	

	public OPERATION_RESPONSE execute() throws EC2Exception{
		if (soapService != null) {
			return executeSOAP();
		} else {
			return executeREST();
		}
	}
    
    private OPERATION_RESPONSE executeREST() throws EC2Exception{
		Map<String, String> parameters = new HashMap<String, String>();
		configureRestRequest(parameters);
		OPERATION_RESPONSE result = null;
		try {
			EC2_RESPONSE ec2Response = restService.getRestService().makeRequest(getHttpMethod(), getEC2MethodName(), parameters, ec2ResponseClass);
			result = processEC2Responce(ec2Response);
		} catch (HttpException e) {
			throw new EC2Exception(e);
		} catch (IOException e) {
			throw new EC2Exception(e);
		} catch (JAXBException e) {
			throw new EC2Exception(e);
		}
		return result;
	}


	/**
	 * Execute request, process faults (if any) and return appropriate result or exception.
	 * 
	 * @return
	 * @throws EC2Exception
	 */
	private OPERATION_RESPONSE executeSOAP() throws EC2Exception{
		// Create request object.
		SOAP_REQUEST request = null;
		try {
			request = soapRequestClass.newInstance();
		} catch (InstantiationException e) {
			throw new EC2Exception(e);
		} catch (IllegalAccessException e) {
			throw new EC2Exception(e);
		}
		
		// Invoke operation and return response.
		int retries = 0;
		boolean done = true;
		boolean retry = false;
		OPERATION_RESPONSE response = null;
		do{
			try{
				configureSoapRequest(request);
				AmazonEC2PortType portType = soapService.getService();
				Method method = null;
				try {
					method = portType.getClass().getMethod(StringUtils.uncapitalize(getEC2MethodName()), soapRequestClass);
					EC2_RESPONSE ec2Response  = (EC2_RESPONSE)method.invoke(portType, request);
					response = processEC2Responce(ec2Response);
				} catch (IllegalArgumentException e) {
					//This should not happen in production
					throw new RuntimeException(e);
				} catch (IllegalAccessException e) {
					//This should not happen in production
					throw new RuntimeException(e);
				} catch (InvocationTargetException e) {
					//This should not happen in production
					throw new RuntimeException(e);
				} catch (SecurityException e) {
					//This should not happen in production
					throw new RuntimeException(e);
				} catch (NoSuchMethodException e) {
					//This should not happen in production
					throw new RuntimeException(e);
				}
			}
			catch(SOAPFaultException e){
				done = true;
				retry = false;
				
				SOAPFault fault = e.getFault();
				if(fault != null){
					String code = fault.getFaultCode();
					logger.debug(code + " - " + fault.getFaultString());
					if(code.indexOf(UNAVAILABLE) > -1){ // - Internal Server Error (equivalent to HTTP 500 error)
						done = false;
						retry = true;
					}
					else if(code.indexOf(INSUFFICIENT_INSTANCE_CAPACITY) > -1){ 
						done = false;
						retry = true;
					}
					else if(code.indexOf(INSUFFICIENT_ADDRESS_CAPACITY) > -1){
						done = false;
						retry = true;
					}
					else if(code.indexOf(INSUFFICIENT_RESERVED_INSTANCE_CAPACITY) > -1){ 
						done = false;
						retry = true;
					}
					else if(code.indexOf(INTERNAL_ERROR) > -1){ 
						done = false;
						retry = true;
					}
					else{
						throw new EC2Exception(fault.getFaultString(), e);
					}
				}
				else{
					throw new EC2Exception(e.getMessage(), e);
				}
			}
			if(retry){
				retries++;
				if (retries > maxRetries) {
					throw new EC2Exception("The number of retries exhausted.");
				}
				retry = false;
				try { 
					Thread.sleep((int)Math.pow(2.0, retries)*1000); 
				} 
				catch (InterruptedException ex) {}
			}
		}
		while(!done);
		
		return response;
	}
	
	protected abstract void configureSoapRequest(SOAP_REQUEST request);

	
	protected abstract void configureRestRequest(Map<String, String> request);
	
	protected abstract String getEC2MethodName();
	
	protected HttpMethodBase getHttpMethod() {
		return new GetMethod();
	}
	
	protected abstract OPERATION_RESPONSE processEC2Responce(EC2_RESPONSE response);
	
	/**
	 * @return the maxRetries
	 */
	public int getMaxRetries() {
		return maxRetries;
	}

	/**
	 * @param maxRetries the maxRetries to set
	 */
	public void setMaxRetries(int maxRetries) {
		this.maxRetries = maxRetries;
	}
}
